package executor

import (
	"context"
	"deploy-tools/config"
	"deploy-tools/model"
	"deploy-tools/remote"
	"deploy-tools/utils"
	"errors"
	"fmt"
	"log"
	"sync"
	"time"
)

type Executor struct {
	Ctx      context.Context
	Cancel   context.CancelFunc
	Recorder *TaskRecorder
}

func NewExecutor() *Executor {
	ctx, cancel := context.WithCancel(context.Background())
	recorder, err := NewRecorder("")
	if err != nil {
		panic(err)
	}

	return &Executor{
		Ctx:      ctx,
		Cancel:   cancel,
		Recorder: recorder,
	}
}

func (e *Executor) Execute(jobName string) {
	hostMap := config.HostMap

	var tasks []func()
	var useHosts = make(map[string]*remote.Client)
	for _, job := range config.GlobalConfig.Jobs {
		if jobName != "" && jobName != job.Name {
			continue
		}
		var hosts []*remote.Client

		if len(job.Hosts) > 0 && job.Hosts[0] == "ALL" {
			job.Hosts = config.AllHosts
		} else {
			hostNames := make([]string, 0)
			for _, tag := range job.Hosts {
				tempHosts := config.HostGroup[tag]
				if tempHosts != nil && len(tempHosts) > 0 {
					hostNames = append(hostNames, tempHosts...)
				} else {
					hostNames = append(hostNames, tag)
				}
			}
			job.Hosts = hostNames
		}

		for _, host := range job.Hosts {
			h := hostMap[host]
			if h == nil {
				log.Printf("no host for alias: %s", host)
				continue
			}

			client := useHosts[host]
			if client == nil {
				client = remote.NewRemote(h)
				timeout := h.Timeout
				if timeout == 0 {
					timeout = 20
				}
				log.Printf("[%s] Connectiong in %d", h.Name, timeout)
				err := client.Connect(timeout)
				if err != nil {
					log.Printf("[%s] Connect Failed: %s", h.Name, err)
					return
				} else {
					useHosts[host] = client
				}
				log.Printf("[%s] Connected", h.Name)
			}
			hosts = append(hosts, client)
		}

		if len(hosts) > 0 {
			tasks = append(tasks, func() {
				e.ExecuteJob(job, hosts)
			})
		} else {
			log.Printf("[%s] No hosts", job.Name)
		}
	}
	for _, task := range tasks {
		task()
	}
	for _, c := range useHosts {
		c.Close()
	}

}

func (e *Executor) ExecuteJob(job *model.Job, hosts []*remote.Client) {
	if job.Parallel {
		var wg sync.WaitGroup
		for index, host := range hosts {
			job.Index = index
			wg.Add(1)
			go e.ExecuteJobOnHost(job, host, &wg)
		}
		wg.Wait()
		return
	}
	for index, host := range hosts {
		job.Index = index
		e.ExecuteJobOnHost(job, host, nil)
	}
}

func (e *Executor) ExecuteJobOnHost(job *model.Job, host *remote.Client, wg *sync.WaitGroup) {
	log.Printf("[%s]-[%s] Execute Start", host.Name, job.Name)
	if wg != nil {
		defer wg.Done()
	}
	for idx, task := range job.Tasks {
		task.Index = idx
		if task.Timeout == 0 {
			task.Timeout = job.Timeout
		}
		recordKey := ""
		if task.RecordKey != "" {
			recordKey = fmt.Sprintf("%s_%s_%s", host.Name, job.RecordKey, task.RecordKey)
		}
		if e.Recorder.IsRecord(recordKey) {
			log.Printf("[%s]-[%s] Execute Last Success, Skip", host.Name, job.Name)
			continue
		}
		select {
		case <-e.Ctx.Done():
			log.Printf("[%s]-[%s] Execute Cancel", host.Name, job.Name)
			return
		default:
			result, err := e.ExecuteTask(job.Name, task, host)
			if err != nil && !task.AllowFail {
				e.Cancel()
				e.Recorder.UnRecord(recordKey)
				return
			}
			e.Recorder.Record(recordKey)
			if task.CacheKey != "" {
				config.CacheVar(task.CacheKey, result)
			}
			break
		}
	}
	log.Printf("[%s]-[%s] Execute Finish", host.Name, job.Name)
}

func (e *Executor) ExecuteTask(jobName string, task *model.Task, host *remote.Client) (string, error) {
	log.Printf("[%s]-[%s]-[%s] Execute Task Start : %s", host.Name, jobName, task.Name, task.Command)

	if task.Echo != "" {
		echo := ReplaceCommand(task.Echo, host)
		log.Printf("[%s]-[%s]-[%s] Echo : %s", host.Name, jobName, task.Name, echo)
	}

	var result = ""
	var err error

	src := ReplaceCommand(task.Src, host)
	dst := ReplaceCommand(task.Dst, host)

	if src != "" && task.Fetch {
		result, err = host.FetchFile(src, dst, task.Override)
	} else if src != "" {
		result, err = host.SendFile(utils.GetAbsolutePath(src), dst)
	} else if task.Command != "" {
		result, err = host.ExecuteIn(ReplaceCommand(task.Command, host), task.Timeout)
	}

	if task.Check != "" {
		if task.Timeout < 1 {
			task.Timeout = 30
		}
		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(task.Timeout)*time.Second)
		defer cancel()
		checkCommand := ReplaceCommand(task.Check, host)
		for {
			select {
			case <-ctx.Done():
				log.Printf("[%s]-[%s]-[%s] Execute Check Timeout", host.Name, jobName, task.Name)
				return "", errors.New("ExecuteCheck timeout")
			default:
				_, err = host.ExecuteIn(checkCommand, task.Timeout)
				if err == nil {
					log.Printf("[%s]-[%s]-[%s] Execute Task Check Ok", host.Name, jobName, task.Name)
					goto OK
				}
				time.Sleep(5 * time.Second)
			}
		}
	} else if err != nil {
		log.Printf("[%s]-[%s]-[%s] Execute Task error: %v", host.Name, jobName, task.Name, err)
		return "", err
	}
OK:
	err = nil
	log.Printf("[%s]-[%s]-[%s] Execute Task Finish", host.Name, jobName, task.Name)
	return result, nil
}
